﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Linq;
using System.Threading;
using CoreEx.Common.Extensions;
using CoreEx.Database.Metadata;
using LinFu.IoC.Configuration;
using LinFu.IoC.Interfaces;
using LinFu.IoC;

namespace CoreEx.Database.MetaData.Implementation
{
    [Implements(typeof(IPropertyInfoMapper))]
    public class PropertyInfoMapper : IPropertyInfoMapper
    {
        private ITableInfoMapper _tableInfoMapper;

        private IDictionary<string, PropertyInfo> _mappedColumns;

        private IDictionary<PropertyInfo, IPropertyMappingInfo> _mappedProperties;

        public PropertyInfoMapper(ITableInfoMapper tableInfoMapper)
        {
            _tableInfoMapper = tableInfoMapper;
        }


        /// <summary>
        /// Maps the properties from the <paramref name="type"/> parameter
        /// to a list of <see cref="IPropertyMappingInfo"/> instances.
        /// </summary>
        /// <param name="type">The type that contains the properties to be mapped.</param>
        /// <param name="tableInfo">The <see cref="ITableInfo"/> instance that corresponds to the <paramref name="type"/>.</param>
        /// <returns>
        /// A list of <see cref="IPropertyMappingInfo"/> instances.
        /// </returns>
        public IDictionary<PropertyInfo, IPropertyMappingInfo> MapFrom(Type type, ITableInfo tableInfo)
        {
            _mappedColumns = new Dictionary<string, PropertyInfo>();
            _mappedProperties = new Dictionary<PropertyInfo, IPropertyMappingInfo>();
            var properties = type.GetProperties();
            MapPrimitiveProperties(properties, tableInfo);           
            MapReferenceProperties(properties, tableInfo);
            MapEnumerableProperties(properties, tableInfo);
            ValidatePrimaryKey(type,tableInfo);
            return _mappedProperties;
        }



        private void ValidatePrimaryKey(Type type, ITableInfo tableInfo)
        {
            if (tableInfo.PrimaryKey == null)
                throw new MappingException(string.Format("Unable to map type '{0}'. The table '{1}' does not define a primary key", type.Name, tableInfo));
            var primaryKeyColumns = tableInfo.PrimaryKey.Columns;


            var test = primaryKeyColumns.Where(pkc => !_mappedColumns.Keys.Contains(pkc.ColumnName));
            
            if (primaryKeyColumns.Where(pkc => !_mappedColumns.Keys.Contains(pkc.ColumnName)).Count() > 0)
            throw new MappingException(
                string.Format(
                    "Unable to map type '{0}'. The type must declare properties(s) to hold the primary key value(s)",type.Name));
        }


        private void MapEnumerableProperties(IEnumerable<PropertyInfo> properties, ITableInfo tableInfo)
        {
            var enumerableProperties = properties.Where(p => p.PropertyType.IsEnumerable()).ToList();
            foreach (var enumerableProperty in enumerableProperties)
            {
                if (!enumerableProperty.PropertyType.GetGenericElementType().IsSystemType())
                    MapEnumerableProperty(enumerableProperty,tableInfo);
            }
        }

        private void MapReferenceProperties(IEnumerable<PropertyInfo> properties, ITableInfo tableInfo)
        {
            var referenceProperties = properties.Where(p => !p.PropertyType.IsEnumerable() && 
                !p.PropertyType.IsSystemType() && p.CanRead && p.CanWrite);
            foreach (var referenceProperty in referenceProperties)
            {
                MapReferenceProperty(referenceProperty,tableInfo);
            }
        }

        private void MapReferenceProperty(PropertyInfo propertyInfo, ITableInfo tableInfo)
        {
            var cardinality = CardinalityType.ManyToOne;
            var primaryTableInfo = _tableInfoMapper.MapFrom(propertyInfo.PropertyType);

            //Find all foreignkeys that references the tableinfo represented by the property type.
            var foreignkeyCandidates = tableInfo.ForeignKeys.Where(fk => fk.PrimaryTable == primaryTableInfo);

            IForeignKeyInfo foreignKeyInfo = null;

            //No foreignkey is found and we assume a OneToOne cardinality
            //where the foreignkey is defined on the "many" side of the relation.
            if (foreignkeyCandidates.Count() == 0)
            {
                foreignKeyInfo =
                    primaryTableInfo.ForeignKeys.Where(fk => fk.PrimaryTable == tableInfo).FirstOrDefault();
                cardinality = CardinalityType.OneToOne;
            }
            else
            {
                //Find all columns represented by the foreign key candidates.
                var foreignKeyColumns = foreignkeyCandidates.SelectMany(fk => fk.Columns);

                //Find the foreignKey columnname that best matches the property name.
                var foreignKeyColumnName =
                    propertyInfo.Name.GetBestMatch(foreignKeyColumns.Select(c => c.ForeignColumnInfo.ColumnName));

                //Find the foreign key column that matches the foreign key column name.
                var foreignKeyColumnInfo =
                    foreignKeyColumns.Where(c => c.ForeignColumnInfo.ColumnName == foreignKeyColumnName).FirstOrDefault();

                foreignKeyInfo = foreignKeyColumnInfo.ForeignKey;
            }

            if (foreignKeyInfo == null) 
                return;
            
            var propertyMappingInfo = new PropertyMappingInfo
                                          {
                                              ForeignKeyInfo = foreignKeyInfo,
                                              RelationType = cardinality,
                                              Property = propertyInfo
                                          };
            _mappedProperties.Add(propertyInfo, propertyMappingInfo);
        }


        private void MapEnumerableProperty(PropertyInfo propertyInfo, ITableInfo tableInfo)
        {
            var listElementType = propertyInfo.PropertyType.GetGenericElementType();
            var foreignTableInfo = _tableInfoMapper.MapFrom(listElementType);
            
            //Find the foreign key that refers to the target table
            var foreignKeyInfo = foreignTableInfo.ForeignKeys.Where(fk => fk.PrimaryTable == tableInfo).FirstOrDefault();
            var cardinality = CardinalityType.OneToMany;
            if (foreignKeyInfo == null)            
            {
                //Find all tables that has a foreign key to the table represented by the tableInfo parameter
                var junctionTableCandidates = tableInfo.Relations
                                            .Where(r => r.PrimaryKey != tableInfo.PrimaryKey).ToList().Select(r => r.PrimaryKey.Table).ToList();                

                
                //Find the junction table that refers to a table that matches the table represented by the foreigntableInfo
                var juctionTable = junctionTableCandidates.SelectMany(t => t.ForeignKeys).Where(
                                            f => f.PrimaryTable == foreignTableInfo).Select(fk => fk.Table).FirstOrDefault();

                if (juctionTable != null)
                {
                    //Finally select the foreignkey in the junction table that refers back to the table represented by the tableinfo parameter.
                    foreignKeyInfo = juctionTable.ForeignKeys.Where(
                        fk => fk.PrimaryTable == tableInfo).FirstOrDefault();
                    cardinality = CardinalityType.ManyToMany;
                }
            }

            if (foreignKeyInfo == null)
                return; 
            
            var propertyMappingInfo = new PropertyMappingInfo
                                          {
                                              ForeignKeyInfo = foreignKeyInfo,
                                              Property = propertyInfo,
                                              RelationType = cardinality
                                          };
            _mappedProperties.Add(propertyInfo,propertyMappingInfo);        
        }


        
        private void MapPrimitiveProperties(IEnumerable<PropertyInfo> properties, ITableInfo tableInfo)
        {                        
            var primitiveProperties =
                properties.Where(p => p.PropertyType.IsSystemType() && !p.PropertyType.IsEnumerable() && p.CanRead && p.CanWrite).ToList();
            
            var columnNames = tableInfo.Columns.Select(c => c.Value.ColumnName).ToList();
            foreach (var property in primitiveProperties)
            {
                MapPrimitiveProperty(property, columnNames, tableInfo);
            }


            var primaryKeyColumns = tableInfo.PrimaryKey.Columns.Select(c => c.ColumnName);
            var foreignKeyColumns =
                tableInfo.ForeignKeys.SelectMany(fk => fk.Columns).Select(c => c.ForeignColumnInfo.ColumnName).Except(primaryKeyColumns);
            

            var remainingColumns = columnNames.Except(_mappedColumns.Keys).Except(foreignKeyColumns).ToList();
            if (remainingColumns.Count() == 0)
                return;
            var remainingProperties = primitiveProperties.Except(_mappedColumns.Values).ToList();
            if (remainingProperties.Count() == 0)
                return;
            foreach (var remainingProperty in remainingProperties)            
                MapPrimitiveProperty(remainingProperty,remainingColumns,tableInfo);
            

        }

        private void MapPrimitiveProperty(PropertyInfo propertyInfo, IEnumerable<string> columnNames, ITableInfo tableInfo)
        {                        
            var propertyName = propertyInfo.Name;            
            //Find the column name that best matches the property name.
            var columnName = propertyName.GetBestMatch(columnNames);
            

            //Check to see if the column already has been mapped to another property.
            if (_mappedColumns.ContainsKey(columnName))
            {
                var mappedPropertyName = _mappedColumns[columnName].Name;
                //Find the columnname that best matches the two candidates
                var bestMatchingPropertyName = columnName.GetBestMatch(new[] {propertyName, mappedPropertyName});
                
                //This property represents a better match that the previous mapping
                if (bestMatchingPropertyName == propertyName)
                {
                    _mappedColumns.Remove(columnName);
                    _mappedProperties.Remove(propertyInfo.DeclaringType.GetProperty(mappedPropertyName));
                }
                else
                    return;
            }

            var propertyMappingInfo = new PropertyMappingInfo();
            propertyMappingInfo.ColumnInfo = tableInfo.Columns[columnName];
            propertyMappingInfo.Property = propertyInfo;
            propertyMappingInfo.RelationType = CardinalityType.None;                    

            _mappedColumns.Add(columnName,propertyInfo);
            _mappedProperties.Add(propertyInfo,propertyMappingInfo);           
        }




        
    }
}
